/*
 * Copyright (c) 2012, Dennis Lee & Christopher Uyeda
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification
 * are permitted provided that the following conditions are met:
 * 		* Redistributions of source code must retain the above copyright notice,
 * 		  this list of conditions and the following disclaimer.
 * 		* Redistributions in binary form must reproduce the above copyright notice,
 * 		  this list of conditions and the following disclaimer in the documentation
 * 		  and/or other materials provided with the distribution.
 *		* Neither the name of the University of Hawaii at Manoa or the names of
 * 		  its contributors may be used to endorse or promote products derived from
 *		  this software without specific prior written permission.
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package quickstats;

import java.util.Arrays;
import java.util.TreeSet;

/**
 * MonovariateCalculator
 *
 * @author Dennis Lee & Christopher Uyeda
 * @date November 29th 2012
 * @abstract Computes descriptive statistics (monovariate analysis) on a given
 * double array
 */
public class MonovariateCalculator {

    private double[] sample;
    private int samplesize;
    private double sum;
    private double mean;
    private double median;
    private String mode;
    private double max;
    private double min;
    private double range;
    private double geometricmean;
    private double skewness;
    private double kurtosis;
    private double excesskurtosis;
    private double variance;
    private double stddev;
    private double firstquartile;
    private double secondquartile;
    private double thirdquartile;
    private double fourthquartile;
    private double firstinterquartilerange;
    private double secondinterquartilerange;
    private double thirdinterquartilerange;
    private double fourthinterquartilerange;
    private String frequencydist;
    private boolean loaded;
    private boolean computed;

    /*
     * constructor
     */
    public MonovariateCalculator() {
        this.loaded = false;
        this.computed = false;
    }

    /*
     * compute
     * computes various monovariate statistics
     * returns true if successful, false if not
     */
    public boolean compute() {

        this.computed = false; //reset computed flag

        if (!this.loaded) {
            return false;
        }

        try {
            computeSampleSize();
            computeSum();
            computeMean();
            computeMedian();
            computeMode();
            computeMax();
            computeMin();
            computeRange();
            computeGeometricMean();
            computeSkewness();
            computeKurtosis();
            computeExcessKurtosis();
            computeVariance();
            computeStddev();
            computeQuartiles();
            computeInterquartilerange();
            computeFrequencydist();
        } catch (Exception e) {
            return false;
        }

        this.computed = true;   //set computed flag
        return true;
    }

    /*
     * computeSampleSize
     * computes the size of the sample
     */
    private void computeSampleSize() {
        this.samplesize = this.sample.length;
    }

    /*
     * computeSum
     * computes the total of the sample
     */
    private void computeSum() {
        double temp = 0;

        for (int i = 0; i < this.sample.length; i++) {
            temp += this.sample[i];
        }

        this.sum = temp;
    }

    /*
     * computeMean
     * computes the mean of the sample
     */
    private void computeMean() {
        double temp = 0;

        for (int i = 0; i < this.sample.length; i++) {
            temp += this.sample[i];
        }

        this.mean = temp / this.sample.length;
    }

    /*
     * computeMedian
     * computes the median of the sample
     */
    private void computeMedian() {
        double tempMedian = 0;

        Arrays.sort(this.sample);

        if (this.sample.length % 2 == 0) {
            int mid1 = this.sample.length / 2;
            int mid2 = (this.sample.length / 2) - 1;
            tempMedian = (this.sample[mid1] + this.sample[mid2]) / 2; //takes two mid points and averages them
        } else if ((this.sample.length % 2 != 0) && this.sample.length > 2) {
            tempMedian = this.sample[(int) this.sample.length / 2];
        }
        this.median = tempMedian;
    }

    /*
     * computeMode
     * computes the mode(s) of the sample
     */
    private void computeMode() {

        /* Add elements to TreeSet */
        TreeSet<Double> modesTreeSet = new TreeSet<Double>();
        int maxCount = 0;
        for (int i = 0; i < this.sample.length; ++i) {
            int count = 0;
            for (int j = 0; j < this.sample.length; ++j) {
                if (this.sample[j] == this.sample[i]) {
                    ++count;
                }
            }
            if (count > maxCount) {
                maxCount = count;
                modesTreeSet.clear();
                modesTreeSet.add(this.sample[i]);
            } else if (count == maxCount) {
                modesTreeSet.add(this.sample[i]);
            }
        }

        /* Add elements to string */
        this.mode = modesTreeSet.toString();
    }

    /*
     * computeMax
     * computes the greatest element of the sample
     */
    private void computeMax() {
        double tempMax = sample[0];
        for (int i = 0; i < sample.length; i++) {
            if (sample[i] > tempMax) {
                tempMax = sample[i];
            }
        }
        this.max = tempMax;
    }

    /*
     * computeMin
     * computes the smallest element of the sample
     */
    private void computeMin() {
        double tempMin = sample[0];
        for (int i = 0; i < sample.length; i++) {
            if (sample[i] < tempMin) {
                tempMin = sample[i];
            }
        }

        this.min = tempMin;
    }

    /*
     * computeRange
     * computes the difference betten the min and max element of the sample
     */
    private void computeRange() {
        this.range = Math.abs(this.max - this.min);
    }

    /*
     * computeGeometricMean
     * computes the geometric mean of the sample
     */
    private void computeGeometricMean() {

        double tempsum = 1;

        for (int i = 0; i < this.sample.length; i++) {
            tempsum *= this.sample[i];
        }

        this.geometricmean = Math.pow(tempsum, 1.0 / this.sample.length);
    }

    /*
     * computeSkewness
     * computes the skewness of the sample
     */
    private void computeSkewness() {
        double numerator = 0;
        double denominator = 0;

        for (int i = 0; i < this.sample.length; i++) {
            numerator += Math.pow(this.sample[i] - this.mean, 3); // deviations to the third power
            denominator += Math.pow(this.sample[i] - this.mean, 2); // deviations to the second power
        }

        numerator = (1.0 / sample.length) * numerator; // multiply by 1/ data size

        denominator = Math.pow(denominator * (1.0 / sample.length), 3.0 / 2.0); // multiply by 1/data size and raise to the power of 1.5

        this.skewness = (numerator / denominator);
    }

    /*
     * computeKurtosis
     * computes the skewness of the sample
     */
    private void computeKurtosis() {
        double numerator = 0;
        double denominator = 0;

        for (int i = 0; i < this.sample.length; i++) {
            numerator += Math.pow(this.sample[i] - mean, 4); // deviation to the fourth power
            denominator += Math.pow(this.sample[i] - mean, 2); // deviations to the second power
        }

        numerator = (double) (1.0 / this.sample.length) * numerator; // multiple by 1/data length

        denominator = Math.pow(denominator * (double) (1.0 / this.sample.length), 2); // multiply by 1/data length and square

        this.kurtosis = (numerator / denominator);
    }

    /*
     * computeExcessKurtosis
     * computes the skewness of the sample
     */
    private void computeExcessKurtosis() {
        this.excesskurtosis = this.kurtosis - 3;
    }

    /*
     * computeVariance
     * computes the difference betten the min and max element of the sample
     */
    private void computeVariance() {
        double tempVariance = 0;

        for (int i = 0; i < sample.length; i++) {
            tempVariance += (this.mean - sample[i]) * (this.mean - sample[i]);
        }
        this.variance = tempVariance / this.sample.length;
    }

    /*
     * computeStddev
     * computes the standard deviation of the elements of the sample
     */
    private void computeStddev() {
        double tempStddev = 0;

        for (int i = 0; i < sample.length; i++) {
            tempStddev += Math.pow((sample[i] - this.mean), 2);
        }

        this.stddev = Math.sqrt((tempStddev / sample.length));
    }

    /*
     * computeQuartiles
     * computes the four quartile values cutoff of the sample
     */
    private void computeQuartiles() {
        Arrays.sort(this.sample);
        this.firstquartile = this.sample[(int) Math.floor(0.25 * (double) this.sample.length)];
        this.secondquartile = this.sample[(int) Math.floor(0.50 * (double) this.sample.length)];
        this.thirdquartile = this.sample[(int) Math.floor(0.75 * (double) this.sample.length)];
        this.fourthquartile = this.sample[this.sample.length - 1];
    }

    /*
     * computeInterquartilerange
     * computes the four quartile values cutoff of the sample
     */
    private void computeInterquartilerange() {
        this.firstinterquartilerange = this.firstquartile - this.sample[0];
        this.secondinterquartilerange = this.secondquartile - this.firstquartile;
        this.thirdinterquartilerange = this.thirdquartile - this.secondquartile;
        this.fourthinterquartilerange = this.fourthquartile - this.thirdquartile;
    }

    /*
     * computeFrequencydist
     * computes the frequency distribution of the sample
     */
    private void computeFrequencydist() {
        Arrays.sort(this.sample);
        StringBuilder sb = new StringBuilder();
        String temp;

        for (int i = 0; i < this.sample.length; i++) {
            int count = 1;
            sb.append("     ");
            temp = Double.toString(this.sample[i]);
            while ((i < this.sample.length - 1)
                    && (this.sample[i] == this.sample[i + 1])) {
                count++;
                i++;
            }

            sb.append(count);
            sb.append("\t\t");
            sb.append(temp);
            sb.append("\n");
        }
        this.frequencydist = sb.toString();
    }

    /*
     * getSampleSize
     * returns the current sample size
     */
    public int getSampleSize() {
        return this.samplesize;
    }

    /*
     * getSum
     * returns the sum of the sample
     */
    public double getSum() {
        return this.sum;
    }

    /*
     * getMean
     * returns the mean of the sample
     */
    public double getMean() {
        return this.mean;
    }

    /*
     * getMedian
     * returns the median of the sample
     */
    public double getMedian() {
        return this.median;
    }

    /*
     * getMode
     * returns the mode(s) of the sample
     */
    public String getMode() {
        return this.mode;
    }

    /*
     * getMax
     * returns the greatest valued element of the sample
     */
    public double getMax() {
        return this.max;
    }

    /*
     * getMin
     * returns the smallest valued element of the sample 
     */
    public double getMin() {
        return this.min;
    }

    /*
     * getRange
     * returns the difference between the max and min elements
     */
    public double getRange() {
        return this.range;
    }

    /*
     * getSkewness
     * returns the difference between the max and min elements
     */
    public double getSkewness() {
        return this.skewness;
    }

    /*
     * getKurtosis
     * returns the kurtosis of the sample
     */
    public double getKurtosis() {
        return this.kurtosis;
    }

    /*
     * getExceessKurtosis
     * returns the kurtosis of the sample
     */
    public double getExcessKurtosis() {
        return this.excesskurtosis;
    }

    /*
     * getGeometricMean
     * returns the geometric mean of the sample
     */
    public double getGeometricMean() {
        return this.geometricmean;
    }

    /*
     * getVariance
     * returns the variance of the sample
     */
    public double getVariance() {
        return this.variance;
    }


    /*
     * getStddev
     * returns the standard deviation of the sample
     */
    public double getStddev() {
        return this.stddev;
    }

    /*
     * getFirstQuartile
     * returns the first quartile values cutoff of the sample   
     */
    public double getFirstQuartile() {
        return this.firstquartile;
    }

    /*
     * getSecondQuartile
     * returns the second quartile values cutoff of the sample   
     */
    public double getSecondQuartile() {
        return this.secondquartile;
    }

    /*
     * getThirdQuartile
     * returns the third quartile values cutoff of the sample   
     */
    public double getThirdQuartile() {
        return this.thirdquartile;
    }

    /*
     * getForthQuartile
     * returns the fourth quartile values cutoff of the sample   
     */
    public double getForthQuartile() {
        return this.fourthquartile;
    }

    /*
     * getFirstInterquartilerange
     * retuns the first interquartile range of the sample
     */
    public double getFirstInterquartileRange() {
        return this.firstinterquartilerange;
    }

    /*
     * getSecondInterquartilerange
     * retuns the second interquartile range of the sample
     */
    public double getSecondInterquartileRange() {
        return this.secondinterquartilerange;
    }

    /*
     * getThirdInterquartilerange
     * retuns the third interquartile range of the sample
     */
    public double getThirdInterquartileRange() {
        return this.thirdinterquartilerange;
    }

    /*
     * getFourthInterquartilerange
     * retuns the fourth interquartile range of the sample
     */
    public double getFourthInterquartileRange() {
        return this.fourthinterquartilerange;
    }

    /*
     * getFrequencydist
     * returns the frequency distribution of the sample
     */
    public String getFrequencydist() {
        return this.frequencydist;
    }

    /*
     * load
     * loads new file at given filepath argument
     * returns true if successful, false if not
     */
    public boolean load(double[] sample) {
        /* reset loaded flag */
        this.loaded = false;

        /* create new array from provided sample */
        try {
            this.sample = sample;
        } catch (Exception e) {
            return false;
        }

        /* set loaded flag and return success */
        this.loaded = true;
        return true;
    }

    /*
     * toString
     * returns a prinout of the monovariate statistics of the sample
     */
    public String toString() {

        if (!this.computed) {
            String errormsg = "Could not compute input";
            return errormsg;
        }

        StringBuilder sb = new StringBuilder("");

        String printout = ""
                + "Sample Size:\t\t" + this.samplesize + "\n"
                + "Sum:\t\t" + this.sum + "\n"
                + "Mean:\t\t" + this.mean + "\n"
                + "Median:\t\t" + this.median + "\n"
                + "Mode(s):\t\t" + this.mode + "\n"
                + "Max:\t\t" + this.max + "\n"
                + "Min:\t\t" + this.min + "\n"
                + "Range:\t\t" + this.range + "\n"
                + "Geom. Mean:\t\t" + this.geometricmean + "\n"
                + "Skewness:\t\t" + this.skewness + "\n"
                + "Kurtosis:\t\t" + this.kurtosis + "\n"
                + "Excess Kurtosis:\t" + this.excesskurtosis + "\n"
                + "Variance:\t\t" + this.variance + "\n"
                + "Std Deviation:\t\t" + this.stddev + "\n"
                + "Quartiles:\n"
                + "     " + "1st:\t\t" + this.firstquartile + "\n"
                + "     " + "2nd:\t\t" + this.secondquartile + "\n"
                + "     " + "3rd:\t\t" + this.thirdquartile + "\n"
                + "     " + "4th:\t\t" + this.fourthquartile + "\n"
                + "Interquartile Range:\n"
                + "     " + "0-1:\t\t" + this.firstinterquartilerange + "\n"
                + "     " + "1-2:\t\t" + this.secondinterquartilerange + "\n"
                + "     " + "2-3:\t\t" + this.thirdinterquartilerange + "\n"
                + "     " + "3-4:\t\t" + this.fourthinterquartilerange + "\n"
                + "Frequency Distribution:\n"
                + this.frequencydist;

        sb.append("[MONOVARIATE ANALYSIS]\n");
        sb.append(printout);
        return sb.toString();
    }
}
